package com.jdjc.subject.infra.basic.service.impl;


import com.jdjc.subject.common.entity.PageResult;
import com.jdjc.subject.common.enums.SubjectInfoTypeEnum;
import com.jdjc.subject.infra.basic.entity.EsSubjectFields;
import com.jdjc.subject.infra.basic.entity.SubjectInfoEs;
import com.jdjc.subject.infra.basic.es.EsIndexInfo;
import com.jdjc.subject.infra.basic.es.EsRestClient;
import com.jdjc.subject.infra.basic.es.EsSearchRequest;
import com.jdjc.subject.infra.basic.es.EsSourceData;
import com.jdjc.subject.infra.basic.service.SubjectEsService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;


@Service("subjectEsService")
@Slf4j
public class SubjectEsServiceImpl implements SubjectEsService {


    @Override
    public boolean insert(SubjectInfoEs subjectInfoEs) {
        EsSourceData esSourceData = new EsSourceData();
        Map<String, Object> data = convert2EsSourceData(subjectInfoEs);
        esSourceData.setDocId(subjectInfoEs.getDocId().toString());
        esSourceData.setData(data);
        return EsRestClient.insertDoc(getEsIndexInfo(), esSourceData);
    }


    /**
     * es 实现带高亮的网站全文检索
     *
     * @param subjectInfoEs
     * @return
     */
    @Override
    public PageResult<SubjectInfoEs> querySubjectList(SubjectInfoEs subjectInfoEs) {
        PageResult<SubjectInfoEs> pageResult = new PageResult<>();
        // 创建一个搜索请求对象，并使用createSearchListQuery方法来构建查询
        EsSearchRequest esSearchRequest = createSearchListQuery(subjectInfoEs);
        // 使用Elasticsearch客户端执行搜索请求，获取搜索响应
        SearchResponse searchResponse = EsRestClient.searchWithTermQuery(getEsIndexInfo(), esSearchRequest);
        // 创建一个SubjectInfoEs类型的列表，用于存储搜索结果
        List<SubjectInfoEs> subjectInfoEsList = new LinkedList<>();
        // 从搜索响应中获取搜索命中结果
        SearchHits searchHits = searchResponse.getHits();

        // 如果搜索命中结果为空，则设置分页信息和总记录数为0，并返回分页结果
        if (searchHits == null || searchHits.getHits() == null) {
            pageResult.setPageNo(subjectInfoEs.getPageNo());
            pageResult.setPageSize(subjectInfoEs.getPageSize());
            pageResult.setRecords(subjectInfoEsList);
            pageResult.setTotal(0);
            return pageResult;
        }

        // 获取搜索命中的文档数组
        SearchHit[] hits = searchHits.getHits();

        // 遍历搜索命中的文档
        for (SearchHit hit : hits) {
            // 将搜索命中的文档转换为SubjectInfoEs对象
            SubjectInfoEs infoEs = convertResult(hit);
            // 如果转换结果不为空，则添加到列表中
            if (Objects.nonNull(infoEs)) {
                subjectInfoEsList.add(infoEs);
            }
        }

        // 设置分页信息，包括当前页码、每页记录数、记录列表和总记录数
        pageResult.setPageNo(subjectInfoEs.getPageNo()); // 设置当前页码
        pageResult.setPageSize(subjectInfoEs.getPageSize()); // 设置每页记录数
        pageResult.setRecords(subjectInfoEsList); // 设置记录列表
        pageResult.setTotal(Long.valueOf(searchHits.getTotalHits().value).intValue()); // 设置总记录数


        return pageResult;
    }

    /**
     * 将Elasticsearch的搜索命中转换为一个SubjectInfoEs对象
     * @param hit
     * @return
     */
    private SubjectInfoEs convertResult(SearchHit hit) {
        // 从搜索命中结果中获取源数据，并将其转换为Map对象
        Map<String, Object> sourceAsMap = hit.getSourceAsMap();
        // 如果源数据Map为空，说明没有有效的源数据，因此返回null
        if (CollectionUtils.isEmpty(sourceAsMap)) {
            return null;
        }

        SubjectInfoEs result = new SubjectInfoEs();
        result.setSubjectId(MapUtils.getLong(sourceAsMap, EsSubjectFields.SUBJECT_ID));
        result.setSubjectName(MapUtils.getString(sourceAsMap, EsSubjectFields.SUBJECT_NAME));
        result.setSubjectAnswer(MapUtils.getString(sourceAsMap, EsSubjectFields.SUBJECT_ANSWER));
        result.setDocId(MapUtils.getLong(sourceAsMap, EsSubjectFields.DOC_ID));
        result.setSubjectType(MapUtils.getInteger(sourceAsMap, EsSubjectFields.SUBJECT_TYPE));
        // 计算es相关性分数，并将结果设置到result对象中
        result.setScore(new BigDecimal(String.valueOf(hit.getScore())).multiply(new BigDecimal("100.00")).setScale(2, RoundingMode.HALF_DOWN));

        // 处理题目名字段的高亮显示,从命中结果中获取高亮显示的字段
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();

        // 从高亮字段Map中获取与题目名字段对应的高亮信息。
        HighlightField subjectNameField = highlightFields.get(EsSubjectFields.SUBJECT_NAME);

        // 如果题目名字段的高亮信息不为null，即存在高亮信息，则进行以下处理。
        if (subjectNameField != null) {
            // 获取题目名字段高亮信息的片段数组。这些片段是已经用高亮标签包裹好的文本片段。
            Text[] fragments = subjectNameField.getFragments();
            // 创建一个StringBuilder对象，用于拼接高亮片段。
            StringBuilder subjectNameBuilder = new StringBuilder();
            // 遍历所有高亮片段，将它们拼接成一个完整的字符串。
            for (Text fragment : fragments) {
                subjectNameBuilder.append(fragment);
            }
            // 将拼接好的高亮字符串设置到结果对象的题目名字段中。
            result.setSubjectName(subjectNameBuilder.toString());
        }
        // 处理题目答案字段的高亮显示
        HighlightField subjectAnswerField = highlightFields.get(EsSubjectFields.SUBJECT_ANSWER);
        if (subjectAnswerField != null) {
            Text[] fragments = subjectAnswerField.getFragments();
            StringBuilder subjectAnswerBuilder = new StringBuilder();
            for (Text fragment : fragments) {
                subjectAnswerBuilder.append(fragment);
            }
            result.setSubjectAnswer(subjectAnswerBuilder.toString());
        }

        return result;
    }

    /**
     * 在Elasticsearch中使用Java设置复杂的搜索查询，
     * 包括使用布尔逻辑组合不同的查询类型（匹配查询），并对搜索结果中的关键词进行高亮显示。
     * @param subjectInfoEs
     * @return
     */
    private EsSearchRequest createSearchListQuery(SubjectInfoEs subjectInfoEs) {
        log.info("sujectInfoEs : {}", subjectInfoEs);
        // 创建一个Elasticsearch搜索请求对象
        EsSearchRequest esSearchRequest = new EsSearchRequest();

        // 创建一个查询条件构建器
        BoolQueryBuilder bq = new BoolQueryBuilder();

        // 创建一个匹配查询构建器，用于查询题目名称字段
        MatchQueryBuilder subjectNameQueryBuilder = QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_NAME, subjectInfoEs.getKeyWord());
        // 将题目名称查询作为布尔查询中的一个可选条件，并提高其权重
        bq.should(subjectNameQueryBuilder);
        subjectNameQueryBuilder.boost(2);

        // 创建一个匹配查询构建器，用于查询题目答案字段
        MatchQueryBuilder subejctAnswerQueryBuilder = QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_ANSWER, subjectInfoEs.getKeyWord());
        // 将题目答案查询作为布尔查询中的一个可选条件
        bq.should(subejctAnswerQueryBuilder);

        // 创建一个匹配查询构建器，用于查询题目类型字段
        MatchQueryBuilder subejctTypeQueryBuilder = QueryBuilders.matchQuery(EsSubjectFields.SUBJECT_TYPE, SubjectInfoTypeEnum.BRIEF.getCode());
        // 将题目类型查询作为布尔查询中的一个必须条件
        bq.must(subejctTypeQueryBuilder);

        // 设置布尔查询中至少需要匹配的条件数量
        bq.minimumShouldMatch(1);

        // 创建一个高亮构建器，用于设置高亮显示的字段和样式
        //.field("*")：这部分设置了高亮字段。在这里，"*"代表对所有字段进行高亮。这意味着Elasticsearch将在搜索结果中所有匹配的字段中对关键词进行高亮显示。
        //requireFieldMatch(false)表示不需要字段匹配。这意味着即使某个字段不是搜索查询中指定的字段，只要该字段中包含了搜索关键词，Elasticsearch也会对其进行高亮显示。
        HighlightBuilder highlightBuilder = new HighlightBuilder().field("*").requireFieldMatch(false);
        highlightBuilder.preTags("<span style=\"color:red\">"); // 设置高亮开始标签
        highlightBuilder.postTags("</span>"); // 设置高亮结束标签

        // 设置搜索请求中的查询条件
        esSearchRequest.setBq(bq);
        // 设置搜索请求中的高亮构建器
        esSearchRequest.setHighlightBuilder(highlightBuilder);
        // 设置查询字段
        esSearchRequest.setFields(EsSubjectFields.FIELD_QUERY);
        // 设置分页起始位置
        esSearchRequest.setFrom((subjectInfoEs.getPageNo() - 1) * subjectInfoEs.getPageSize());
        // 设置每页显示的记录数
        esSearchRequest.setSize(subjectInfoEs.getPageSize());
        // 设置是否需要快照
        esSearchRequest.setNeedScroll(false);

        return esSearchRequest;
    }

    /**
     * 指定同步的es集群和索引的名字
     *
     * @return
     */
    private EsIndexInfo getEsIndexInfo() {
        EsIndexInfo esIndexInfo = new EsIndexInfo();
        esIndexInfo.setClusterName("4b149801808d");
        esIndexInfo.setIndexName("subject_index");
        return esIndexInfo;
    }

    /**
     * 将SubjecInfoEs映射为map
     *
     * @param subjectInfoEs
     * @return
     */
    private Map<String, Object> convert2EsSourceData(SubjectInfoEs subjectInfoEs) {
        Map<String, Object> data = new HashMap<>();
        data.put(EsSubjectFields.SUBJECT_ID, subjectInfoEs.getSubjectId());
        data.put(EsSubjectFields.DOC_ID, subjectInfoEs.getDocId());
        data.put(EsSubjectFields.SUBJECT_NAME, subjectInfoEs.getSubjectName());
        data.put(EsSubjectFields.SUBJECT_ANSWER, subjectInfoEs.getSubjectAnswer());
        data.put(EsSubjectFields.SUBJECT_TYPE, subjectInfoEs.getSubjectType());
        data.put(EsSubjectFields.CREATE_USER, subjectInfoEs.getCreateUser());
        data.put(EsSubjectFields.CREATE_TIME, subjectInfoEs.getCreateTime());
        return data;
    }
}
